﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Drawing;
using System.Text;

namespace SharpGL.Text
{
    /// <summary>
    /// This class wraps the functionality of the wglUseFontBitmaps function to
    /// allow straightforward rendering of text.
    /// </summary>
    public class FontBitmaps : IDisposable
    {
        #region Internal Classes
        
        /// <summary>
        /// A FontBitmap entry contains the details of a font face.
        /// </summary>
        internal class FontBitmapEntry
        {
            public uint ListBase
            {
                get;
                set;
            }

            public uint FirstGlyph
            {
                get;
                set;
            }

            public uint NumOfGlyphs
            {
                get;
                set;
            }

            public Font Font
            {
                get;
                set;
            }
        }

        #endregion

        #region Plivate Members

        private OpenGL currentOpenGLContext = null;
        
        private List<FontBitmapEntry> fontBitmapEntries = new List<FontBitmapEntry>();

        private uint firstFontGlyph = 0;
        private uint numOfFontGlyphs = 255;

        #endregion

        #region Private Methods
        
        /// <summary>
        /// Create bitmap font in provided OpenGL context.
        /// </summary>
        /// <param name="gl">The OpenGL context.</param>
        /// <param name="firstGlyph">The first glyph.</param>
        /// <param name="numOfGlyphs">The number of glyphs.</param>
        private FontBitmapEntry CreateFontBitmapEntry(OpenGL gl, Font font, uint firstGlyph, uint numOfGlyphs)
        {
            uint listBase = 0;
            IntPtr hFont = IntPtr.Zero;
            IntPtr hOldObject = IntPtr.Zero;
            FontBitmapEntry fbe = null;

            try
            {
                if (!OpenGL.IsValidContext(currentOpenGLContext) || null == font)
                    throw new System.ArgumentException("OpenGL context and font must not be null.");

                //  Make the OpenGL instance current.
                gl.MakeCurrent();

                //  Generate OpenGL list
                listBase = gl.GenLists((int)numOfGlyphs);
                if (0 == listBase)
                    throw new System.Exception("Can't generate new OpenGL display list.");

                //  Get font handle.
                hFont = font.ToHfont();

                //  Select the font handle.
                hOldObject = Win32.SelectObject(gl.RenderContextProvider.DeviceContextHandle, hFont);

                //  Create the font bitmaps.
                if (false == Win32.wglUseFontBitmapsW(gl.RenderContextProvider.DeviceContextHandle, firstGlyph, numOfGlyphs, listBase))
                    throw new System.Exception("Can't create font bitmaps.");

                //  Reselect the old font.
                Win32.SelectObject(gl.RenderContextProvider.DeviceContextHandle, hOldObject);
                hOldObject = IntPtr.Zero;

                //  Free the font.
                Win32.DeleteObject(hFont);
                hFont = IntPtr.Zero;

                //  Create the font bitmap entry.
                fbe = new FontBitmapEntry()
                {
                    ListBase = listBase,
                    FirstGlyph = firstGlyph,
                    NumOfGlyphs = numOfGlyphs,
                    Font = (Font)font.Clone()
                };
            }
            catch
            {
                if (0 != listBase)
                    gl.DeleteLists(listBase, (int)numOfGlyphs);
                fbe = null;
            }
            finally
            {
                if (IntPtr.Zero != hOldObject)
                    Win32.SelectObject(gl.RenderContextProvider.DeviceContextHandle, hOldObject);
                if (IntPtr.Zero != hFont)
                    Win32.DeleteObject(hFont);
            }

            return fbe;
        }

        #endregion

        #region Construction/Destruction

        /// <summary>
        /// Constructor
        /// </summary>
        public FontBitmaps()
        {
            currentOpenGLContext = null;
        }

        /// <summary>
        /// Destructor
        /// </summary>
        ~FontBitmaps()
        {
            Destroy();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates the bitmaps font in OpenGL instance.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        /// <returns>True if successfully.</returns>
        public bool Create(OpenGL gl)
        {
            if (OpenGL.IsValidContext(currentOpenGLContext))
                return false;

            currentOpenGLContext = gl;

            return true;
        }
        
        /// <summary>
        /// Destroy the bitmaps font.
        /// </summary>
        public void Destroy()
        {
            if (OpenGL.IsValidContext(currentOpenGLContext))
            {
                foreach (FontBitmapEntry fbe in fontBitmapEntries)
                {
                    currentOpenGLContext.DeleteLists(fbe.ListBase, (int)fbe.NumOfGlyphs);
                    ((IDisposable)fbe.Font).Dispose();
                }
            }

            fontBitmapEntries.Clear();

            currentOpenGLContext = null;
        }

        public void Dispose()
        {
            Destroy();

            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Draws the text.
        /// </summary>
        /// <param name="x">The x.</param>
        /// <param name="y">The y.</param>
        /// <param name="font">The font.</param>
        /// <param name="text">The text.</param>
        public void DrawText(int x, int y, Font font, string text)
        {
            if (!OpenGL.IsValidContext(currentOpenGLContext) || null == text)
                return;
            
            //  Do we have a font bitmap entry for this OpenGL instance and font?
            var fbeQuery = from fbe in fontBitmapEntries
                         where fbe.Font.Equals(font)
                         select fbe;

            //  Get the FBE or null.
            FontBitmapEntry fontBitmapEntry = (fbeQuery == null || fbeQuery.Count() == 0 ? null : fbeQuery.First());

            //  If we don't have the FBE, we must create it.
            if (fontBitmapEntry == null)
            {
                fontBitmapEntry = CreateFontBitmapEntry(currentOpenGLContext, font, firstFontGlyph, numOfFontGlyphs);
                //  Add the font bitmap entry to the internal list.
                if (null != fontBitmapEntry)
                    fontBitmapEntries.Add(fontBitmapEntry);
            }

            // If we don't found FBE and can't create it do nothing
            if (null == fontBitmapEntry)
                return;

            //  Create an array of lists for the glyphs.
            List<uint> lists = new List<uint>();
            foreach (char c in text)
                lists.Add((uint)c);

            //  Check text for illegal characters.
            foreach (uint ch in lists)
            {
                int index = (int)ch - (int)fontBitmapEntry.FirstGlyph;
                if (0 > index || index >= fontBitmapEntry.NumOfGlyphs)
                    return;
            }

            //  Save current atributes and set new one
            currentOpenGLContext.PushAttrib(OpenGL.GL_LIST_BIT | OpenGL.GL_CURRENT_BIT |
                OpenGL.GL_ENABLE_BIT | OpenGL.GL_TRANSFORM_BIT);
            currentOpenGLContext.Disable(OpenGL.GL_LIGHTING);
            currentOpenGLContext.Disable(OpenGL.GL_TEXTURE_2D);
            currentOpenGLContext.Disable(OpenGL.GL_DEPTH_TEST);

            //  Create the appropriate projection matrix.
            currentOpenGLContext.MatrixMode(OpenGL.GL_PROJECTION);
            currentOpenGLContext.PushMatrix();
            currentOpenGLContext.LoadIdentity();
            double width = currentOpenGLContext.RenderContextProvider.Width;
            double height = currentOpenGLContext.RenderContextProvider.Height;
            currentOpenGLContext.Ortho(0, width, 0, height, -1, 1);

            //  Create the appropriate modelview matrix.
            currentOpenGLContext.MatrixMode(OpenGL.GL_MODELVIEW);
            currentOpenGLContext.PushMatrix();
            currentOpenGLContext.LoadIdentity();

            //  Set first glyph position
            currentOpenGLContext.RasterPos(x, y);

            //  Set the list base.
            currentOpenGLContext.ListBase(fontBitmapEntry.ListBase - fontBitmapEntry.FirstGlyph);

            //  Call the lists for the string.
            currentOpenGLContext.CallLists(lists.Count, lists.ToArray());
            //currentOpenGLContext.Flush();

            //  Pop the modelview.
            currentOpenGLContext.PopMatrix();

            //  Back to the projection and pop it.
            currentOpenGLContext.MatrixMode(OpenGL.GL_PROJECTION);
            currentOpenGLContext.PopMatrix();

            //  Restore atributes.
            currentOpenGLContext.PopAttrib();
        }

        public Size GetTextSize(Font font, string text)
        {
            if (!OpenGL.IsValidContext(currentOpenGLContext) || null == font || null == text)
                return Size.Empty;

            Size result;

            using (Graphics graphics = Graphics.FromHdc(currentOpenGLContext.RenderContextProvider.DeviceContextHandle))
            {
                graphics.PageUnit = GraphicsUnit.Pixel;
                graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias;
                
                SizeF size = graphics.MeasureString(text, font);

                result = size.ToSize();
            }
            
            return result;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the current OpenGL context.
        /// </summary>
        public OpenGL CurrentOpenGLContext
        {
            get { return currentOpenGLContext; }
        }
        
        public uint FirstFontGlyph
        {
            get { return firstFontGlyph; }
            set { firstFontGlyph = value; }
        }
        
        public uint NumberOfFontGlyphs
        {
            get { return numOfFontGlyphs; }
            set { numOfFontGlyphs = value; }
        }

        #endregion
    }
}
